// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Data;
using SnapObjects.Commons;

namespace SnapObjects.Data.Odbc
{
    public class OdbcSqlAnywhereTypeMapper : TypeMapper
    {
        //string
        private readonly OdbcStringTypeMapping _unboundedUnicodeString = new OdbcStringTypeMapping("nvarchar(max)", dbType: null, unicode: true);
        private readonly OdbcStringTypeMapping _keyUnicodeString = new OdbcStringTypeMapping("nvarchar(450)", dbType: null, unicode: true, size: 450);
        private readonly OdbcStringTypeMapping _unboundedAnsiString = new OdbcStringTypeMapping("varchar(max)", dbType: DbType.AnsiString);
        private readonly OdbcStringTypeMapping _keyAnsiString = new OdbcStringTypeMapping("varchar(900)", dbType: DbType.AnsiString, unicode: false, size: 900);
        private readonly OdbcByteArrayTypeMapping _unboundedBinary = new OdbcByteArrayTypeMapping("varbinary(max)");
        private readonly OdbcByteArrayTypeMapping _keyBinary = new OdbcByteArrayTypeMapping("varbinary(900)", dbType: DbType.Binary, size: 900);
        private readonly OdbcByteArrayTypeMapping _rowversion = new OdbcByteArrayTypeMapping("rowversion", dbType: DbType.Binary, size: 8);
        private readonly OdbcCharTypeMapping _char = new OdbcCharTypeMapping("char", dbType: DbType.AnsiString);
        private readonly OdbcStringTypeMapping _fixedLengthUnicodeString = new OdbcStringTypeMapping("nchar", dbType: DbType.String, unicode: true);
        private readonly OdbcStringTypeMapping _variableLengthUnicodeString = new OdbcStringTypeMapping("nvarchar", dbType: null, unicode: true);
        private readonly OdbcLongStringTypeMapping _longVariableLengthUnicodeString = new OdbcLongStringTypeMapping("long nvarchar", dbType: null, unicode: true);
        private readonly OdbcStringTypeMapping _fixedLengthAnsiString = new OdbcStringTypeMapping("char", dbType: DbType.AnsiString);
        private readonly OdbcStringTypeMapping _variableLengthAnsiString = new OdbcStringTypeMapping("varchar", dbType: DbType.AnsiString);
        private readonly OdbcLongStringTypeMapping _longVariableLengthAnsiString = new OdbcLongStringTypeMapping("long varchar", dbType: DbType.AnsiString);

        //number
        private readonly IntTypeMapping _int = new IntTypeMapping("int", DbType.Int32);
        private readonly LongTypeMapping _long = new LongTypeMapping("bigint", DbType.Int64);
        private readonly ShortTypeMapping _short = new ShortTypeMapping("smallint", DbType.Int16);
        private readonly ByteTypeMapping _byte = new ByteTypeMapping("tinyint", DbType.Byte);
        private readonly OdbcFloatTypeMapping _float = new OdbcFloatTypeMapping("float", DbType.Single);
        private readonly OdbcDoubleTypeMapping _double = new OdbcDoubleTypeMapping("double", DbType.Double);
        private readonly DecimalTypeMapping _decimal = new DecimalTypeMapping("decimal", DbType.Decimal);

        //binary
        private readonly OdbcByteArrayTypeMapping _variableLengthBinary = new OdbcByteArrayTypeMapping("varbinary");
        private readonly OdbcByteArrayTypeMapping _fixedLengthBinary = new OdbcByteArrayTypeMapping("binary");
        private readonly OdbcLongByteArrayTypeMapping _longBinary = new OdbcLongByteArrayTypeMapping("long binary");
        private readonly OdbcByteArrayTypeMapping _hierarchyidBinary = new OdbcByteArrayTypeMapping("hierarchyid");
      
        //date
        private readonly SqlAnywhereDateTimeTypeMapping _date = new SqlAnywhereDateTimeTypeMapping("date", dbType: DbType.Date);
        private readonly SqlAnywhereDateTimeTypeMapping _datetime = new SqlAnywhereDateTimeTypeMapping("datetime", dbType: DbType.DateTime);
        private readonly SqlAnywhereDateTimeTypeMapping _datetime2 = new SqlAnywhereDateTimeTypeMapping("datetime2", dbType: DbType.DateTime2);
        private readonly OdbcDateTimeOffsetTypeMapping _datetimeoffset = new OdbcDateTimeOffsetTypeMapping("datetimeoffset");
        private readonly SqlAnywhereTimeSpanTypeMapping _time = new SqlAnywhereTimeSpanTypeMapping("time", DbType.Time);

        //other
        private readonly BoolTypeMapping _bool = new BoolTypeMapping("bit", DbType.Boolean);
        private readonly GuidTypeMapping _uniqueidentifier = new GuidTypeMapping("uniqueidentifier", DbType.Guid);

        protected HashSet<string> _disallowedMappings;
        protected Dictionary<string, TypeMapping> _dbTypeMappings;
        protected Dictionary<Type, TypeMapping> _clrTypeMappings;

        public OdbcSqlAnywhereTypeMapper(IDataContextOptions options) : base(options)
        {
            _dbTypeMappings = new Dictionary<string, TypeMapping>(StringComparer.OrdinalIgnoreCase)
            {
                { "bigint", _long },
                { "binary varying", _variableLengthBinary },
                { "binary", _fixedLengthBinary },
                { "hierarchyid", _hierarchyidBinary },
                { "geography", _hierarchyidBinary },
                { "bit", _bool },
                { "char varying", _variableLengthAnsiString },
                { "char", _fixedLengthAnsiString },
                { "character varying", _variableLengthAnsiString },
                { "character", _fixedLengthAnsiString },
                { "date", _date },
                { "datetime", _datetime },
                { "datetime2", _datetime2 },
                { "datetimeoffset", _datetimeoffset },
                { "dec", _decimal },
                { "decimal", _decimal },
                { "double", _double },
                { "float", _float },
                { "image", _longBinary },
                { "int", _int },
                { "integer", _int },
                { "long varbit", _longVariableLengthAnsiString },
                { "long binary", _longBinary },
                { "long nvarchar", _longVariableLengthUnicodeString },
                { "long varchar", _longVariableLengthAnsiString },
                { "money", _decimal },
                { "national char varying", _variableLengthUnicodeString },
                { "national character varying", _variableLengthUnicodeString },
                { "national character", _fixedLengthUnicodeString },
                { "nchar", _fixedLengthUnicodeString },
                { "ntext", _longVariableLengthUnicodeString },
                { "numeric", _decimal },
                { "nvarchar", _variableLengthUnicodeString },
                { "st_geometry", _variableLengthUnicodeString},
                { "real", _float },
                { "rowversion", _rowversion },
                { "smalldatetime", _datetime },
                { "smallint", _short },
                { "smallmoney", _decimal },
                { "text", _longVariableLengthAnsiString },
                { "time", _time },
                { "timestamp", _datetime },
                { "timestamp with time zone", _variableLengthAnsiString },
                { "tinyint", _byte },
                { "uniqueidentifier", _uniqueidentifier },
                { "unsigned bigint", _decimal },
                { "unsigned int", _long },
                { "unsigned smallint", _int },
                { "varbinary", _variableLengthBinary },
                { "varchar", _variableLengthAnsiString },
                { "varbit", _variableLengthAnsiString },
                { "xml", _longVariableLengthUnicodeString }
            };

            // Note: sbyte, ushort, uint, char and ulong type mappings are not supported by SQL Server.
            // We would need the type conversions feature to allow this to work - see https://github.com/aspnet/EntityFramework/issues/242.
            _clrTypeMappings = new Dictionary<Type, TypeMapping>
            {
                { typeof(int), _int },
                { typeof(long), _long },
                { typeof(DateTime), _datetime },
                { typeof(Guid), _uniqueidentifier },
                { typeof(bool), _bool },
                { typeof(byte), _byte },
                { typeof(double), _double },
                { typeof(float), _float },
                { typeof(DateTimeOffset), _datetimeoffset },
                { typeof(short), _short },
                { typeof(decimal), _decimal },
                { typeof(TimeSpan), _time },
                { typeof(char), _char }
            };

            // These are disallowed only if specified without any kind of length specified in parenthesis.
            // This is because we don't try to make a new type from this string and any max length value
            // specified in the model, which means use of these strings is almost certainly an error, and
            // if it is not an error, then using, for example, varbinary(1) will work instead.
            _disallowedMappings = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "binary varying",
                "binary",
                "char varying",
                "char",
                "character varying",
                "character",
                "national char varying",
                "national character varying",
                "national character",
                "nchar",
                "nvarchar",
                "varbinary",
                "varchar"
            };

            this.ByteArrayMapper = new ByteArrayTypeMapper(
               maxBoundedLength: 8000,
               defaultMapping: _unboundedBinary,
               unboundedMapping: _unboundedBinary,
               keyMapping: _keyBinary,
               createBoundedMapping: size => new OdbcByteArrayTypeMapping(
                   "varbinary(" + size + ")",
                   DbType.Binary,
                   size));

            this.StringMapper = new StringTypeMapper(
                maxBoundedAnsiLength: 8000,
                defaultAnsiMapping: _unboundedAnsiString,
                unboundedAnsiMapping: _unboundedAnsiString,
                keyAnsiMapping: _keyAnsiString,
                createBoundedAnsiMapping: size => new OdbcStringTypeMapping(
                    "varchar(" + size + ")",
                    DbType.AnsiString,
                    unicode: false,
                    size: size),
                maxBoundedUnicodeLength: 4000,
                defaultUnicodeMapping: _unboundedUnicodeString,
                unboundedUnicodeMapping: _unboundedUnicodeString,
                keyUnicodeMapping: _keyUnicodeString,
                createBoundedUnicodeMapping: size => new OdbcStringTypeMapping(
                    "nvarchar(" + size + ")",
                    dbType: null,
                    unicode: true,
                    size: size));
        }

        public override IByteArrayTypeMapper ByteArrayMapper { get; }

        public override IStringTypeMapper StringMapper { get; }

        public override void ValidateTypeName(string dbTypeName)
        {
            if (_disallowedMappings.Contains(dbTypeName))
            {
                throw new ArgumentException(OdbcStrings.UnqualifiedDataType(dbTypeName));
            }
        }

        public override TypeMapping FindMapping(Type clrType)
        {
            Check.NotNull(clrType, nameof(clrType));

            clrType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(clrType));

            return clrType == typeof(string)
                ? _unboundedUnicodeString
                : (clrType == typeof(byte[]) ? _unboundedBinary : base.FindMapping(clrType));
        }

        protected override IReadOnlyDictionary<Type, TypeMapping> GetClrTypeMappings()
        {
            return _clrTypeMappings;
        }

        protected override IReadOnlyDictionary<string, TypeMapping> GetDbTypeMappings()
        {
            return _dbTypeMappings;
        }
    }
}
